package io.github.shenbinglife.jlock;

import io.github.shenbinglife.jlock.exception.LockFailedException;
import io.github.shenbinglife.jlock.exception.UnLockFailedError;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

public class JLock implements Lock {
    private List<Lock> locks;

    public JLock() {
        throw new IllegalArgumentException("locks can not be empty");
    }

    public JLock(Lock... parents) {
        if (parents == null || parents.length == 0) {
            throw new IllegalArgumentException("locks can not be empty");
        } else {
            this.locks = Arrays.asList(parents);
        }
    }

    public JLock(List<Lock> parents) {
        if (parents == null || parents.isEmpty()) {
            throw new IllegalArgumentException("locks can not be null");
        }
        this.locks = new ArrayList<>(parents);
    }

    @Override
    public void lock() {
        Stack<Lock> stack = new Stack<>();

        try {
            for (Lock parent : this.locks) {
                parent.lock();
                stack.add(parent);
            }
        } catch (Throwable e1) {
            while (!stack.isEmpty()) {
                Lock lock = stack.peek();
                try {
                    lock.unlock();
                } catch (Throwable e2) {
                    throw new UnLockFailedError("Unable to unlock parent, dead lock for left locks: " + stack, e2, e1);
                }
                stack.pop();
            }
            throw new LockFailedException("Unable to lock parent's lock", e1);
        }

    }

    @Override
    public void lockInterruptibly() throws InterruptedException {
        Stack<Lock> stack = new Stack<>();

        try {
            for (Lock parent : this.locks) {
                parent.lockInterruptibly();
                stack.add(parent);
            }
        } catch (InterruptedException e) {
            while (!stack.isEmpty()) {
                Lock lock = stack.peek();
                try {
                    lock.unlock();
                } catch (Throwable e2) {
                    throw new UnLockFailedError("Unable to unlock parent, dead lock for left locks: " + stack, e2, e);
                }
                stack.pop();
            }
            throw e;
        } catch (Throwable e1) {
            while (!stack.isEmpty()) {
                Lock lock = stack.peek();
                try {
                    lock.unlock();
                } catch (Throwable e2) {
                    throw new UnLockFailedError("Unable to unlock parent, dead lock for left locks: " + stack, e2, e1);
                }
                stack.pop();
            }
            throw new LockFailedException("Unable to lock parent's lock", e1);
        }
    }

    @Override
    public boolean tryLock() {
        Stack<Lock> stack = new Stack<>();
        boolean locked = true;
        try {
            for (Lock lock : this.locks) {
                boolean b = lock.tryLock();
                if (!b) {
                    locked = false;
                    break;
                } else {
                    stack.push(lock);
                }
            }
        } catch (Throwable e1) {
            while (!stack.isEmpty()) {
                Lock lock = stack.peek();
                try {
                    lock.unlock();
                } catch (Throwable e2) {
                    throw new UnLockFailedError("Unable to unlock parent, dead lock for left locks: " + stack, e2, e1);
                }
                stack.pop();
            }
            throw new LockFailedException("Unable to lock parent's lock", e1);
        }
        if (!locked) {
            while (!stack.isEmpty()) {
                Lock lock = stack.peek();
                try {
                    lock.unlock();
                } catch (Throwable e2) {
                    throw new UnLockFailedError("Unable to unlock parent, dead lock for left locks: " + stack, e2);
                }
                stack.pop();
            }
        }
        return locked;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        long leftMillis = unit.convert(time, TimeUnit.MILLISECONDS);

        Stack<Lock> stack = new Stack<>();
        boolean locked = true;
        try {
            for (Lock lock : this.locks) {
                if (leftMillis <= 0) {
                    locked = false;
                    break;
                }
                long pre = System.currentTimeMillis();
                boolean b = lock.tryLock(leftMillis, TimeUnit.MILLISECONDS);
                long cost = System.currentTimeMillis() - pre;
                leftMillis = leftMillis - cost;
                if (!b) {
                    locked = false;
                    break;
                } else {
                    stack.push(lock);
                }
            }
        } catch (InterruptedException e) {
            while (!stack.isEmpty()) {
                Lock lock = stack.peek();
                try {
                    lock.unlock();
                } catch (Throwable e2) {
                    throw new UnLockFailedError("Unable to unlock parent, dead lock for left locks: " + stack, e2, e);
                }
                stack.pop();
            }
            throw e;
        } catch (Throwable e1) {
            while (!stack.isEmpty()) {
                Lock lock = stack.peek();
                try {
                    lock.unlock();
                } catch (Throwable e2) {
                    throw new UnLockFailedError("Unable to unlock parent, dead lock for left locks: " + stack, e2, e1);
                }
                stack.pop();
            }
            throw new LockFailedException("Unable to lock parent's lock", e1);
        }
        if (!locked) {
            while (!stack.isEmpty()) {
                Lock lock = stack.peek();
                try {
                    lock.unlock();
                } catch (Throwable e2) {
                    throw new UnLockFailedError("Unable to unlock parent, dead lock for left locks: " + stack, e2);
                }
                stack.pop();
            }
        }
        return locked;
    }

    @Override
    public void unlock() {
        List<Lock> reverse = new ArrayList<>(locks);
        Collections.reverse(reverse);

        for (Lock lock : reverse) {
            try {
                lock.unlock();
            } catch (Throwable e2) {
                throw new UnLockFailedError("Unable to unlock parent, dead lock for left lock: " + lock, e2);
            }
        }
    }

    @Override
    public Condition newCondition() {
        return locks.get(locks.size() - 1).newCondition();
    }

    public List<Lock> getLocks() {
        return new ArrayList<>(locks);
    }
}
